package qy.jalgotrade.eventprof;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.tuple.Pair;

import qy.jalgotrade.bar.Bars;
import qy.jalgotrade.barfeed.BaseBarFeed;
import qy.jalgotrade.dataseries.BarDataSeries;
import qy.jalgotrade.dataseries.DataSeries;
import qy.jalgotrade.dataseries.SequenceDataSeries;
import qy.jalgotrade.event.Dispatcher;
import qy.jalgotrade.event.EventHandler;
import qy.jalgotrade.technical.RateOfChange;

/**
 * This class is responsible for scanning over historical data and analyzing returns before and
 * after the events.
 * 
 * @author c-geo
 *
 */
public class Profiler implements EventHandler {

	private Predicate __predicate;

	private int __lookBack;

	private int __lookForward;

	private BaseBarFeed __feed;

	/**
	 * return (not cumulative) series for each instrument:
	 */
	private Map<String, DataSeries<Double>> __rets;

	/**
	 * returns to calculate (a task queue) of occured event for each instrument:
	 */
	private Map<String, List<Pair<Event, Integer>>> __futureRets;

	/**
	 * occured event list for each instrument:
	 */
	private Map<String, List<Event>> __events;

	/**
	 * 
	 * @param predicate   A :class:`Predicate` subclass responsible for identifying events.
	 * @param lookBack    The number of bars before the event to analyze. Must be > 0.
	 * @param lookForward The number of bars after the event to analyze. Must be > 0.
	 */
	public Profiler(Predicate predicate, int lookBack, int lookForward) {

		assert lookBack > 0 && lookForward > 0;
		__predicate = predicate;
		__lookBack = lookBack;
		__lookForward = lookForward;
		__feed = null;
		__rets = new HashMap<>();
		__futureRets = new HashMap<>();
		__events = new HashMap<>();
	}

	/**
	 * 
	 * @param instrument
	 * @param event
	 */
	private void __addPastReturns(String instrument, Event event) {

		int begin = -(event.getLookBack() + 1);
		for (int i = begin; i < 0; i++) {
			try {
				double ret = __rets.get(instrument).__getitem__(i);
				if (!Double.isNaN(ret)) {
					event.setValue(i + 1, ret);
				}
			} catch (IndexOutOfBoundsException e) {

			}
		}
	}

	/**
	 * 
	 * @param instrument
	 */
	private void __addCurrentReturns(String instrument) {

		List<Pair<Event, Integer>> nextTs = new ArrayList<>();
		for (Pair<Event, Integer> e : __futureRets.get(instrument)) {
			Event event = e.getLeft();
			int t = e.getRight();
			event.setValue(t, __rets.get(instrument).__getitem__(-1));
			if (t < event.getLookForward()) {
				nextTs.add(Pair.of(event, t + 1));
			}
		}
		__futureRets.put(instrument, nextTs);
	}

	/**
	 * 
	 * @param dateTime
	 * @param bars
	 */
	private void __onBars(ZonedDateTime dateTime, Bars bars) {

		for (String instrument : bars.getInstruments()) {
			__addCurrentReturns(instrument);
			boolean eventOccurred = __predicate.eventOccurred(instrument,
			        (BarDataSeries) __feed.__getitem__(instrument));
			if (eventOccurred) {
				Event event = new Event(dateTime, __lookBack, __lookForward);
				__events.get(instrument).add(event);
				__addPastReturns(instrument, event);
				// Add next return for this instrument at t = 1.
				__futureRets.get(instrument).add(Pair.of(event, 1));
			}
		}
	}

	/**
	 * Returns the results of the analysis.
	 * 
	 * @return
	 */
	public Results getResults() {

		return new Results(__events, __lookBack, __lookForward);
	}

	/**
	 * Runs the analysis using the bars supplied by the feed.
	 * 
	 * @param feed                       The bar feed to use to run the analysis.
	 * @param useAdjustedCloseForReturns True if adjusted close values should be used to calculate
	 *                                   returns.
	 * @throws Exception
	 */
	public void run(BaseBarFeed feed, boolean useAdjustedCloseForReturns) throws Exception {

		if (useAdjustedCloseForReturns) {
			assert feed.barsHaveAdjClose() : "Feed doesn't have adjusted close values";
		}

		try {
			__feed = feed;
			__rets = new HashMap<>();
			__futureRets = new HashMap<>();
			for (String instrument : feed.getRegisteredInstruments()) {
				__events.putIfAbsent(instrument, new ArrayList<>());
				__futureRets.put(instrument, new ArrayList<>());
				DataSeries<Double> ds = null;
				if (useAdjustedCloseForReturns) {
					ds = ((BarDataSeries) feed.__getitem__(instrument)).getAdjCloseDataSeries();
				} else {
					ds = ((BarDataSeries) feed.__getitem__(instrument)).getCloseDataSeries();
				}
				__rets.put(instrument, new RateOfChange((SequenceDataSeries<Double>) ds, 1));
			}

			feed.getNewValuesEvent().subscribe(this);
			Dispatcher disp = new Dispatcher();
			disp.addSubject(feed);
			disp.run();

		} finally {
			feed.getNewValuesEvent().unsubscribe(this);
		}
	}

	/*
	 * 
	 */
	@Override
	public void handle(String eventName, Map<String, Object> params) throws Exception {

		__onBars((ZonedDateTime) params.get("dateTime"), (Bars) params.get("values"));
	}
}
